// Copyright (c) 2009, Philip Tucker
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the <organization> nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY Philip Tucker 'AS IS' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL Philip Tucker BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package edu.luc.android;

import android.graphics.Point;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;

/**
 * Converts touch events into click, down amd long click. Also hacks around the
 * maddening habit of MapView to drop listeners.
 */
public abstract class MapViewListener implements OnTouchListener {
  /**
   * Touch event must last this long to be considered a long click".
   */
  private static final int LONG_CLICK_THRESHOLD_MILLIS = 1000;
  
  /**
   * Touch event must move nor more than this to be considered a "long click".
   */
  private static final double LONG_CLICK_MAX_MOVE_PIXELS = 5.0d;

  private final MapView mapView;
  private long selectedTimeMillis = -1;
  private GeoPoint geoSelection = null;
  private Point pixelSelection = null;
  private boolean isLongClick = false;

  public MapViewListener(MapView mapView) {
    this.mapView = mapView;
  }
  
  private void clearSelection() {
    selectedTimeMillis = -1;
    geoSelection = null;
    pixelSelection = null;
    isLongClick = false;
  }

  private void startSelection(MotionEvent event) {
    selectedTimeMillis = event.getEventTime();
    int x = Math.round(event.getX());
    int y = Math.round(event.getY());
    geoSelection = mapView.getProjection().fromPixels(x, y);
    pixelSelection = new Point(x, y);
    isLongClick = false;
  }

  @Override
  public String toString() {
    return geoSelection == null ? "no geoSelection" : geoSelection.toString();
  }

  protected abstract boolean onDown(GeoPoint point);  

  protected abstract boolean onClick(GeoPoint point);  

  protected abstract boolean onLongClick(GeoPoint point);  

  @Override
  public boolean onTouch(View v, MotionEvent event) {
    assert v == mapView : "wrong onTouch map view";
    boolean result = false;
    int action = event.getAction();
    switch (action) {
      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_OUTSIDE:
        clearSelection();
        break;
      case MotionEvent.ACTION_DOWN:
        startSelection(event);
        result = onDown(geoSelection);
        break;
      case MotionEvent.ACTION_MOVE:
        if (isNewSelection(event)) {
          startSelection(event);
        } else {
          isLongClick = isLongClick(event);
        }
        break;
      case MotionEvent.ACTION_UP:
        assert geoSelection != null : "no geoSelection";
        if (isLongClick || isLongClick(event)) {
          result = onLongClick(geoSelection);
        } else {
          result = onClick(geoSelection);
        }
        clearSelection();
        break;
    }
    return result;
  }

  /**
   * @return True if we have no geoSelection, or we've moved so far from the
   *         original geoSelection that we have a new one.
   */
  private boolean isNewSelection(MotionEvent event) {
    if (geoSelection == null || pixelSelection == null) {
      // We have no selection.
      return true;
    }

    double deltaX = event.getX() - pixelSelection.x;
    double deltaY = event.getY() - pixelSelection.y;
    double delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    if (delta > LONG_CLICK_MAX_MOVE_PIXELS) {
      // We've moved too far for a single long click.
      return true;
    }

    return false;
  }

  private boolean isLongClick(MotionEvent event) {
    if (selectedTimeMillis <= -1) {
      return false;
    }

    long duration = event.getEventTime() - selectedTimeMillis;
    return (duration >= LONG_CLICK_THRESHOLD_MILLIS);
  }
}
